﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using CodePlex.WebServices.Client;
using EnvDTE;
using EnvDTE80;
using Ionic.Zip;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using VSCPPublishPlugin.CodePlex;
using VSCPPublishPlugin.Ui;
using File = System.IO.File;
using Process = EnvDTE.Process;
using VSShellPackage = Microsoft.VisualStudio.Shell.Package;

namespace VSCPPublishPlugin
{
	/// <summary>
	/// This is the class that implements the package exposed by this assembly.
	///
	/// The minimum requirement for a class to be considered a valid package for Visual Studio
	/// is to implement the IVsPackage interface and register itself with the shell.
	/// This package uses the helper classes defined inside the Managed Package Framework (MPF)
	/// to do it: it derives from the Package class that provides the implementation of the 
	/// IVsPackage interface and uses the registration attributes defined in the framework to 
	/// register itself and its components with the shell.
	/// </summary>
	// This attribute tells the registration utility (regpkg.exe) that this class needs
	// to be registered as package.
	[PackageRegistration(UseManagedResourcesOnly = true)]
	// A Visual Studio component can be registered under different regitry roots; for instance
	// when you debug your package you want to register it in the experimental instance. This
	// attribute specifies the registry root to use if no one is provided to regpkg.exe with
	// the /root switch.

	// This attribute is used to register the informations needed to show the this package
	// in the Help/About dialog of Visual Studio.
	[InstalledProductRegistration("#100", "#102", "1.0", IconResourceID = 400)]
	// In order be loaded inside Visual Studio in a machine that has not the VS SDK installed, 
	// package needs to have a valid load key (it can be requested at 
	// http://msdn.microsoft.com/vstudio/extend/). This attributes tells the shell that this 
	// package has a load key embedded in its resources.

	[ProvideAutoLoad("f1536ef8-92ec-443c-9ed7-fdadf150da82")]
	// This attribute is needed to let the shell know that this package exposes some menus.
	[ProvideMenuResource(1000, 1)]
	// This attribute registers a tool window exposed by this package.
	[Guid(GuidList.PackageGuidString)]
	public sealed class VSCPPublishPluginPackage : VSShellPackage
	{
		internal OleMenuCommand PublishMenuCommand = null;
		internal OleMenuCommand ConfigureMenuCommand = null;
		internal OleMenuCommand ProfileComboMenuCommand = null;


		internal bool _inProcess = false;

		#region Interaction helpers

		internal EnvDTE.DTE GetDTE()
		{
			return VSShellPackage.GetGlobalService(typeof(EnvDTE.DTE)) as DTE;
		}

		internal OleMenuCommandService GetMenuService()
		{
			return GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
		}
		#endregion


		/// <summary>
		/// Default constructor of the package.
		/// Inside this method you can place any initialization code that does not require 
		/// any Visual Studio service because at this point the package object is created but 
		/// not sited yet inside Visual Studio environment. The place to do all the other 
		/// initialization is the Initialize method.
		/// </summary>
		public VSCPPublishPluginPackage()
		{
			//Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering constructor for: {0}", this.ToString()));
		}


		#region Override Package implementations

		protected override void Initialize()
		{

			//Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
			base.Initialize();


			// Add our command handlers for menu (commands must exist in the .vsct file)
			var mcs = GetMenuService();
			if (null != mcs)
			{
				var menuPublishCommandId = new CommandID(GuidList.CommandSetGuid, (int)PackageCommandIDList.cmdidPublishButton);
				PublishMenuCommand = new OleMenuCommand(OnPublish, menuPublishCommandId);
				PublishMenuCommand.ParametersDescription = "$"; // accept any argument string
				PublishMenuCommand.BeforeQueryStatus += PublishMenuCommand_BeforeQueryStatus;
				mcs.AddCommand(PublishMenuCommand);

				var menuConfigureProfilesCommandId = new CommandID(GuidList.CommandSetGuid, (int)PackageCommandIDList.cmdidConfigureProfilesButton);
				ConfigureMenuCommand = new OleMenuCommand(OnConfigureProfiles, menuConfigureProfilesCommandId);
				ConfigureMenuCommand.ParametersDescription = "$"; // accept any argument string
				//ConfigureMenuCommand.BeforeQueryStatus += ConfigureMenuCommand_BeforeQueryStatus;
				mcs.AddCommand(ConfigureMenuCommand);



				// DropDownCombo
				//	 a DROPDOWNCOMBO does not let the user type into the combo box; they can only pick from the list.
				//   The string value of the element selected is returned.
				//	 For example, this type of combo could be used for the "Solution Configurations" on the "Standard" toolbar.
				//
				//   A DropDownCombo box requires two commands:
				//     One command (cmdidMyCombo) is used to ask for the current value of the combo box and to 
				//     set the new value when the user makes a choice in the combo box.
				//
				//     The second command (cmdidMyComboGetList) is used to retrieve this list of choices for the combo box.
				// 
				// Normally IOleCommandTarget::QueryStatus is used to determine the state of a command, e.g.
				// enable vs. disable, shown vs. hidden, etc. The QueryStatus method does not have any way to 
				// control the statue of a combo box, e.g. what list of items should be shown and what is the 
				// current value. In order to communicate this information actually IOleCommandTarget::Exec
				// is used with a non-NULL varOut parameter. You can think of these Exec calls as extended 
				// QueryStatus calls. There are two pieces of information needed for a combo, thus it takes
				// two commands to retrieve this information. The main command id for the command is used to 
				// retrieve the current value and the second command is used to retrieve the full list of 
				// choices to be displayed as an array of strings.
				var menuProfilesListComboCommandId = new CommandID(GuidList.CommandSetGuid, (int)PackageCommandIDList.cmdidProfilesListCombo);
				ProfileComboMenuCommand = new OleMenuCommand(OnMenuProfilesListCombo, menuProfilesListComboCommandId);
				ProfileComboMenuCommand.ParametersDescription = "$"; // accept any argument string
				//ProfileComboMenuCommand.BeforeQueryStatus += ProfileComboMenuCommand_BeforeQueryStatus;
				mcs.AddCommand(ProfileComboMenuCommand);

				var menuProfilesListComboGetListCommandId = new CommandID(GuidList.CommandSetGuid, (int)PackageCommandIDList.cmdidProfilesListComboGetList);
				var menuProfilesListComboGetListCommand = new OleMenuCommand(OnMenuProfilesListComboGetList, menuProfilesListComboGetListCommandId);
				mcs.AddCommand(menuProfilesListComboGetListCommand);
			}
		}

		#endregion


		#region Commands state events

		/*private void ProfileComboMenuCommand_BeforeQueryStatus(object sender, EventArgs e)
		{
		}*/

		private void PublishMenuCommand_BeforeQueryStatus(object sender, EventArgs e)
		{
			RefreshPublishCommandState();
		}

		private void RefreshPublishCommandState()
		{
			// cant publish with no profile selected, and cannot start publishing when already in the process of publishing
			if (_inProcess || string.IsNullOrEmpty(_currentProfileName))
			{
				EnableCommand(PublishMenuCommand, false);
				return;
			}


			// can publish only when a publishable project is selected
			var dte = GetDTE();

			var ok = false;
			Project proj = null;
			var ary = dte.ActiveSolutionProjects as Array;
			if (null != ary && ary.Length > 0)
			{
				proj = ary.GetValue(0) as Project;
			}

			// FIXME: add checking for making sure that the project can actually be ClickOnce published.
			if (null != proj && null != proj.Object)
			{
				ok = true;
			}

			EnableCommand(PublishMenuCommand, ok);
		}

		/*private void ConfigureMenuCommand_BeforeQueryStatus(object sender, EventArgs e)
		{
		}*/

		private static void EnableCommand(MenuCommand command, bool enabled)
		{
			command.Supported = command.Enabled = enabled;
		}

		#endregion



		#region Commands

		#region Profiles List Combo

		private string[] _allProfiles = null;
		private bool _needReloadProfilesList = false;
		private void GetAllProfiles()
		{
			if (null == _allProfiles || _needReloadProfilesList)
			{
				_allProfiles = ProfileConfigForm.GetAllProfileNames();
				_needReloadProfilesList = false;
			}
		}

		private string _currentProfileName = null;

		// DropDownCombo
		//	 a DROPDOWNCOMBO does not let the user type into the combo box; they can only pick from the list.
		//   The string value of the element selected is returned.
		//	 For example, this type of combo could be used for the "Solution Configurations" on the "Standard" toolbar.
		//
		//   A DropDownCombo box requires two commands:
		//     One command (cmdidMyCombo) is used to ask for the current value of the combo box and to 
		//     set the new value when the user makes a choice in the combo box.
		//
		//     The second command (cmdidMyComboGetList) is used to retrieve this list of choices for the combo box.
		private void OnMenuProfilesListCombo(object sender, EventArgs e)
		{
			if (e == EventArgs.Empty)
			{
				// We should never get here; EventArgs are required.
				throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
			}

			var eventArgs = e as OleMenuCmdEventArgs;

			if (eventArgs != null)
			{
				var newChoice = eventArgs.InValue as string;
				var vOut = eventArgs.OutValue;

				if (vOut != IntPtr.Zero && newChoice != null)
				{
					throw (new ArgumentException(Resources.BothInOutParamsIllegal)); // force an exception to be thrown
				}
				else if (vOut != IntPtr.Zero)
				{
					// when vOut is non-NULL, the IDE is requesting the current value for the combo
					Marshal.GetNativeVariantForObject(_currentProfileName, vOut);
				}

				else if (newChoice != null)
				{
					GetAllProfiles();

					var validChoice = _allProfiles.Any(t => 0 == string.Compare(t, newChoice, StringComparison.InvariantCultureIgnoreCase));
					if (validChoice)
					{
						_currentProfileName = newChoice;
						/*var profile = ProfileConfigForm.LoadProfileSettingsByName(newChoice);
						DoPublish(profile);*/
					}
					else
					{
						_currentProfileName = null;
						throw (new ArgumentException(Resources.ParamNotValidStringInList)); // force an exception to be thrown
					}
					RefreshPublishCommandState();
				}
				else
				{
					// We should never get here
					throw (new ArgumentException(Resources.InOutParamCantBeNULL)); // force an exception to be thrown
				}
			}
			else
			{
				// We should never get here; EventArgs are required.
				throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
			}
		}

		// A DropDownCombo box requires two commands:
		//    This command is used to retrieve this list of choices for the combo box.
		// 
		// Normally IOleCommandTarget::QueryStatus is used to determine the state of a command, e.g.
		// enable vs. disable, shown vs. hidden, etc. The QueryStatus method does not have any way to 
		// control the statue of a combo box, e.g. what list of items should be shown and what is the 
		// current value. In order to communicate this information actually IOleCommandTarget::Exec
		// is used with a non-NULL varOut parameter. You can think of these Exec calls as extended 
		// QueryStatus calls. There are two pieces of information needed for a combo, thus it takes
		// two commands to retrieve this information. The main command id for the command is used to 
		// retrieve the current value and the second command is used to retrieve the full list of 
		// choices to be displayed as an array of strings.
		private void OnMenuProfilesListComboGetList(object sender, EventArgs e)
		{
			if ((null == e) || (e == EventArgs.Empty))
			{
				// We should never get here; EventArgs are required.
				throw (new ArgumentNullException(Resources.EventArgsRequired)); // force an exception to be thrown
			}

			var eventArgs = e as OleMenuCmdEventArgs;

			if (eventArgs != null)
			{
				var inParam = eventArgs.InValue;
				var vOut = eventArgs.OutValue;

				if (inParam != null)
				{
					throw (new ArgumentException(Resources.InParamIllegal)); // force an exception to be thrown
				}
				else if (vOut != IntPtr.Zero)
				{
					GetAllProfiles();
					Marshal.GetNativeVariantForObject(_allProfiles, vOut);
				}
				else
				{
					throw (new ArgumentException(Resources.OutParamRequired)); // force an exception to be thrown
				}
			}

		}

		#endregion

		#region Publish Button

		private void OnPublish(object sender, EventArgs e)
		{
			if (e == EventArgs.Empty)
			{
				// We should never get here; EventArgs are required.
				throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
			}

			var eventArgs = e as OleMenuCmdEventArgs;

			if (eventArgs != null)
			{
				if (null == _currentProfileName)
				{
					ShowMessage("Please select a profile to publish with.", "");
				}
				else
				{
					var profileName = _currentProfileName;
					var profile = ProfileConfigForm.LoadProfileSettingsByName(profileName);
					DoPublish(profile);
				}
			}
			else
			{
				// We should never get here; EventArgs are required.
				throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
			}
		}

		#endregion

		#region Configure Profiles Button

		private void OnConfigureProfiles(object sender, EventArgs e)
		{
			if (e == EventArgs.Empty)
			{
				// We should never get here; EventArgs are required.
				throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
			}

			var eventArgs = e as OleMenuCmdEventArgs;

			if (eventArgs != null)
			{
				using (var configForm = new ProfileConfigForm())
				{
					var result = configForm.ShowDialog();
					if (configForm.NeedReload)
					{
						_needReloadProfilesList = true;
					}
					if (DialogResult.Yes == result)
					{
						// do publishing
						var profileName = configForm.SelectedProfileName;
						var profile = ProfileConfigForm.LoadProfileSettingsByName(profileName);
						DoPublish(profile);
					}
				}
			}
			else
			{
				// We should never get here; EventArgs are required.
				throw (new ArgumentException(Resources.EventArgsRequired)); // force an exception to be thrown
			}
		}

		#endregion



		#endregion


		#region Processing

		private static bool ValidateProfile(ProfileSettings profile)
		{
			var haveAllData = !(string.IsNullOrEmpty(profile.ProfileName) && string.IsNullOrEmpty(profile.Username) && string.IsNullOrEmpty(profile.Password) && string.IsNullOrEmpty(profile.ProjectName) && string.IsNullOrEmpty(profile.CodePlexUrl));
			if (!haveAllData)
				return false;

			haveAllData = !(string.IsNullOrEmpty(profile.ReleaseName) && string.IsNullOrEmpty(profile.ReleaseDescription));
			if (!haveAllData)
				return false;

			// FIXME: check that the data is correct
			return true;
		}
		private void DoPublish(ProfileSettings profile)
		{
			_inProcess = true;
			if (ValidateProfile(profile))
			{
				//ShowMessage("Publishing!", "Publishing using the '" + profile.ProfileName + "' profile.");
				// first, publish it

				string err = null;
				var dte = GetDTE();
				if (null == dte)
				{
					ShowMessage("Unable to catch the environment!", "");
					_inProcess = false;
					return;
				}

				var slnbld2 = dte.Solution.SolutionBuild as SolutionBuild2;
				if (null == slnbld2)
				{
					ShowMessage("Unable to get the current solution", "");
					_inProcess = false;
					return;
				}

				var ary = dte.ActiveSolutionProjects as Array;
				if (null == ary || ary.Length <= 0)
				{
					ShowMessage("No selected project", "");
					_inProcess = false;
					return;
				}

				var proj = ary.GetValue(0) as EnvDTE.Project;
				if (null == proj)
				{
					ShowMessage("Selected project not a project", "");
					_inProcess = false;
					return;
				}

				if (null == proj.Object)
				{
					ShowMessage("Selected project not loaded!", "");
					_inProcess = false;
					return;
				}

				var wasSaved = proj.Saved;

				// ensure valid login info first.

				Api.LoginToken_t token;

				// FIXME: shouldnt have these two profile types...
				Api.ProfileSettings_t apiProfile;
				apiProfile.Username = profile.Username;
				apiProfile.Password = profile.Password;
				apiProfile.ProfileName = profile.ProfileName;
				apiProfile.ProjectName = profile.ProjectName;
				apiProfile.CodePlexUrl = profile.CodePlexUrl;
				apiProfile.OpenReleasePageOnPublish = profile.OpenReleasePageOnPublish;

				if (!Api.TryLogin(apiProfile, out token))
				{
					ShowMessage("Could not authenticate you against the CodePlex site, or the project you entered is not one of your projects!");
					_inProcess = false;
					return;
				}

				IList<Api.CodePlexRelease> releases;
				if (!Api.TryGetReleases(token, out releases))
				{
					ShowMessage("Unable to retrieve list of releases for the project");
					_inProcess = false;
					return;
				}

				if (!profile.ReplaceIfExists)
				{
					var existsSameName = releases.Any((r) => r.Name == profile.ReleaseName);
					if (existsSameName)
					{
						ShowMessage("There is already a release with the name \"" + profile.ReleaseName + "\". If you would like to replace it, check the 'Replace if exists' option and publish again.");
						_inProcess = false;
						return;
					}
				}


				slnbld2.BuildProject(proj.ConfigurationManager.ActiveConfiguration.ConfigurationName, proj.UniqueName, true);
				if (slnbld2.LastBuildInfo != 0)
				{
					ShowMessage("Build failed for " + proj.UniqueName, "");
					_inProcess = false;
					return;
				}

				var prop = GetPublishPropertiesForProject(proj);
				if (null == prop)
				{
					ShowMessage("Could not get publish properties", "");
					_inProcess = false;
					return;
				}

				var projPath = proj.FullName.Substring(0, proj.FullName.LastIndexOfAny(new[] { '/', '\\' }));
				var pubDirName = GeneratePubDirName(projPath);
				var pubPath = Path.Combine(projPath, pubDirName + "\\");

				var pubUrlProp = prop.Item("PublishUrl");
				var pubUrl = (string)pubUrlProp.Value;
				var originalPubPath = Path.Combine(projPath, pubUrl);

				pubUrlProp.Value = pubPath;

				slnbld2.PublishProject(proj.ConfigurationManager.ActiveConfiguration.ConfigurationName, proj.UniqueName, true);
				pubUrlProp.Value = pubUrl;
				if (slnbld2.LastPublishInfo != 0)
				{
					ShowMessage("Publish failed for " + proj.UniqueName, "");
					_inProcess = false;
					return;
				}
				// keep saved
				if (wasSaved && !proj.Saved)
					proj.Save();



				var release = new Api.CodePlexRelease
							  {
								  Name = profile.ReleaseName,
								  Description = profile.ReleaseDescription,
								  Status = profile.ReleaseStatus,
								  ChangesetId = profile.ChangesetId,
								  IsDefault = profile.IsDefaultRelease
							  };



				const string pubFileName = "publish.zip";
				var zipFilePath = Path.Combine(pubPath, pubFileName);
				File.Delete(zipFilePath);

				if (!CompressPublishDirectory(pubPath, zipFilePath))
				{
					// copy the newly generated publish into the existing publish dir. This is to make sure that all publishes are kept.
					MoveDirWithOverwrite(pubPath, originalPubPath);
					//ShowMessage("Unable to compress publish directory!", "");
					_inProcess = false;
					return;
				}
				release.SetClickOnceFile(File.ReadAllBytes(zipFilePath));


				var uploadedOk = Api.CreateRelease(token, release, true);

				File.Delete(zipFilePath);
				// copy the newly generated publish into the existing publish dir. This is to make sure that all publishes are kept.
				MoveDirWithOverwrite(pubPath, originalPubPath);

				if (uploadedOk)
				{
					if (profile.OpenReleasePageOnPublish)
					{
						System.Diagnostics.Process.Start(GetReleaseViewUrl(profile, release.Id));
					}
					ShowMessage("Published to CodePlex!", "");
				}
				else
				{
					ShowMessage("There were errors during publishing to CodePlex", "");
				}
				_inProcess = false;
			}
			else
			{
				ShowMessage("Please select a valid profile and ensure that all the data is filled in correctly!", "");
				_inProcess = false;
			}
		}

		public static void CopyDir(string srcPath, string destPath, bool recursive = true, bool overwrite = true, bool move = false)
		{
			if (!Directory.Exists(destPath))
				Directory.CreateDirectory(destPath);


			var dirs = Directory.GetDirectories(srcPath);
			if (recursive)
			{
				foreach (var dir in dirs)
				{
					CopyDir(dir, Path.Combine(destPath, Path.GetFileName(dir)), true, overwrite, move);
				}
			}

			var files = Directory.GetFiles(srcPath);
			foreach (var file in files)
			{
				var destFilePath = Path.Combine(destPath, Path.GetFileName(file));
				File.Copy(file, destFilePath, true);

				if (move)
					File.Delete(file);
			}


			// non-recursive move cannot delete folder
			if (move && (recursive || dirs.Length == 0))
				Directory.Delete(srcPath, true);
		}


		private static void MoveDirWithOverwrite(string pubPath, string originalPubPath)
		{
			CopyDir(pubPath, originalPubPath, move: true);
		}

		private static string GeneratePubDirName(string projPath)
		{
			var path = System.IO.Path.GetTempPath();
			path = Path.Combine(path, "CPVSPublishAddin", DateTime.Now.ToBinary().ToString());
			return path;
			/*
			var dtm = DateTime.Now.ToBinary();
			var add = 0ul;
			const string fmt = "cpvspubaddin_publish_";
			var curTry = fmt + dtm;
			while (Directory.Exists(Path.Combine(projPath, curTry)))
			{
				if (ulong.MaxValue == add)
				{
					curTry = null;
					break;
				}
				curTry += (add++);
			}

			return curTry;*/
		}

		private bool CompressPublishDirectory(string pubPath, string zipFilePath)
		{
			try
			{
				using (var zip = new ZipFile(zipFilePath))
				{
					zip.AddDirectory(pubPath);
					zip.Save();
				}
			}
			catch (Exception ex)
			{
				ShowMessage("An error occured while compressing the published archive! The error is below:", ex.ToString());
				return false;
			}

			return true;
		}

		private static string GetReleaseViewUrl(ProfileSettings profile, int releaseId)
		{
			return "http://" + profile.CodePlexUrl + ".codeplex.com/releases/view/" + releaseId + "/";
		}

		private static EnvDTE.Properties GetPublishPropertiesForProject(EnvDTE.Project project)
		{
			var pubProb = project.Properties.Item("Publish");
			if (null == pubProb)
			{
				return null;
			}
			return pubProb.Value as EnvDTE.Properties;
		}

		#endregion

		#region Helpers

		// Helper method to show a message box using the SVsUiShell/IVsUiShell service
		public void ShowMessage(string title, string message = "")
		{
			var uiShell = (IVsUIShell)GetService(typeof(SVsUIShell));
			var clsid = Guid.Empty;
			var result = VSConstants.S_OK;
			var hr = uiShell.ShowMessageBox(0,
											ref clsid,
											title,
											message,
											null,
											0,
											OLEMSGBUTTON.OLEMSGBUTTON_OK,
											OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST,
											OLEMSGICON.OLEMSGICON_INFO,
											0,        // false = application modal; true would make it system modal
											out result);
			ErrorHandler.ThrowOnFailure(hr);
		}
		#endregion

	}
}
